@frames.js/debugger
Version:
Debug your Frames.js applications
355 lines (321 loc) • 9.57 kB
text/typescript
import { type SendNotificationRequest } from "@farcaster/frame-sdk";
import type { NextRequest } from "next/server";
import { z } from "zod";
import {
FrameServerEvent,
InvalidWebhookResponseError,
sendEvent,
} from "frames.js/farcaster-v2/events";
import { type Hex, hexToBytes } from "viem";
import { getStorage } from "../storage";
import { notificationDetailsSchema } from "../parsers";
export type NotificationUrl = {
token: string;
};
export type Notification = Omit<SendNotificationRequest, "tokens">;
const privateKeyParser = z
.string()
.min(1)
.startsWith("0x")
.transform((val) => hexToBytes(val as Hex));
const postRequestBodySchema = z.discriminatedUnion("action", [
z.object({
action: z.literal("add_frame"),
privateKey: privateKeyParser,
}),
z.object({
action: z.literal("remove_frame"),
privateKey: privateKeyParser,
}),
z.object({
action: z.literal("enable_notifications"),
privateKey: privateKeyParser,
}),
z.object({
action: z.literal("disable_notifications"),
privateKey: privateKeyParser,
}),
]);
export type POSTNotificationsDetailRequestBody = z.input<
typeof postRequestBodySchema
>;
const postResponseBodySchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("frame_added"),
notificationDetails: notificationDetailsSchema,
}),
z.object({
type: z.literal("frame_removed"),
}),
z.object({
type: z.literal("notifications_enabled"),
notificationDetails: notificationDetailsSchema,
}),
z.object({
type: z.literal("notifications_disabled"),
}),
]);
export type POSTNotificationsDetailResponseBody = z.infer<
typeof postResponseBodySchema
>;
/**
* Handles different operations on namespace level
*/
export async function POST(
req: NextRequest,
{ params }: { params: { namespaceId: string } }
) {
const requestBody = postRequestBodySchema.safeParse(await req.json());
if (!requestBody.success) {
return Response.json(requestBody.error.flatten(), { status: 400 });
}
const storage = await getStorage(req.nextUrl.href);
const namespace = await storage.getNamespace(params.namespaceId);
if (!namespace) {
return Response.json({ message: "Not found" }, { status: 404 });
}
switch (requestBody.data.action) {
case "add_frame": {
const notificationDetails = await storage.addFrame(namespace);
const eventId = crypto.randomUUID();
await storage.recordEvent(namespace.id, {
type: "event",
event: {
event: "frame_added",
notificationDetails,
},
id: eventId,
});
const event: FrameServerEvent = {
event: "frame_added",
notificationDetails,
};
sendEvent(event, {
privateKey: requestBody.data.privateKey,
fid: namespace.fid,
webhookUrl: namespace.webhookUrl,
})
.then(() => {
return storage.recordEvent(namespace.id, {
type: "event_success",
event,
eventId,
id: crypto.randomUUID(),
});
})
.catch(async (e) => {
return storage.recordEvent(namespace.id, {
type: "event_failure",
event,
eventId,
id: crypto.randomUUID(),
message: e instanceof Error ? e.message : String(e),
response:
e instanceof InvalidWebhookResponseError
? {
body: await e.response.text(),
headers: Object.fromEntries(e.response.headers.entries()),
status: e.response.status,
}
: undefined,
});
});
return Response.json(
postResponseBodySchema.parse({
type: "frame_added",
notificationDetails,
}),
{ status: 201 }
);
}
case "remove_frame": {
await storage.removeFrame(namespace);
const eventId = crypto.randomUUID();
const event: FrameServerEvent = {
event: "frame_removed",
};
await storage.recordEvent(namespace.id, {
type: "event",
event,
id: eventId,
});
sendEvent(event, {
fid: namespace.fid,
privateKey: requestBody.data.privateKey,
webhookUrl: namespace.webhookUrl,
})
.then(() => {
return storage.recordEvent(namespace.id, {
type: "event_success",
event,
eventId,
id: crypto.randomUUID(),
});
})
.catch(async (e) => {
return storage.recordEvent(namespace.id, {
type: "event_failure",
event,
eventId,
id: crypto.randomUUID(),
message: e instanceof Error ? e.message : String(e),
response:
e instanceof InvalidWebhookResponseError
? {
body: await e.response.text(),
headers: Object.fromEntries(e.response.headers.entries()),
status: e.response.status,
}
: undefined,
});
});
return Response.json(
postResponseBodySchema.parse({ type: "frame_removed" }),
{
status: 200,
}
);
}
case "enable_notifications": {
const notificationDetails = await storage.enableNotifications(namespace);
const eventId = crypto.randomUUID();
await storage.recordEvent(namespace.id, {
type: "event",
event: {
event: "notifications_enabled",
notificationDetails,
},
id: eventId,
});
const event: FrameServerEvent = {
event: "notifications_enabled",
notificationDetails,
};
sendEvent(event, {
fid: namespace.fid,
privateKey: requestBody.data.privateKey,
webhookUrl: namespace.webhookUrl,
})
.then(() => {
return storage.recordEvent(namespace.id, {
type: "event_success",
event,
eventId,
id: crypto.randomUUID(),
});
})
.catch(async (e) => {
return storage.recordEvent(namespace.id, {
type: "event_failure",
event,
eventId,
id: crypto.randomUUID(),
message: e instanceof Error ? e.message : String(e),
response:
e instanceof InvalidWebhookResponseError
? {
body: await e.response.text(),
headers: Object.fromEntries(e.response.headers.entries()),
status: e.response.status,
}
: undefined,
});
});
return Response.json(
postResponseBodySchema.parse({
type: "notifications_enabled",
notificationDetails,
}),
{ status: 201 }
);
}
case "disable_notifications": {
await storage.disableNotifications(namespace);
const eventId = crypto.randomUUID();
await storage.recordEvent(namespace.id, {
type: "event",
event: {
event: "notifications_disabled",
},
id: eventId,
});
const event: FrameServerEvent = {
event: "notifications_disabled",
};
sendEvent(event, {
fid: namespace.fid,
privateKey: requestBody.data.privateKey,
webhookUrl: namespace.webhookUrl,
})
.then(() => {
return storage.recordEvent(namespace.id, {
type: "event_success",
event,
eventId,
id: crypto.randomUUID(),
});
})
.catch(async (e) => {
return storage.recordEvent(namespace.id, {
type: "event_failure",
event,
eventId,
id: crypto.randomUUID(),
message: e instanceof Error ? e.message : String(e),
response:
e instanceof InvalidWebhookResponseError
? {
body: await e.response.text(),
headers: Object.fromEntries(e.response.headers.entries()),
status: e.response.status,
}
: undefined,
});
});
return Response.json(
postResponseBodySchema.parse({ type: "notifications_disabled" }),
{
status: 200,
}
);
}
default: {
requestBody.data satisfies never;
throw new Error(`Unknown action`);
}
}
}
export const getResponseBodySchema = z.object({
fid: z.number(),
frameAppUrl: z.string().url(),
namespaceUrl: z.string().url(),
webhookUrl: z.string().url(),
frame: z.discriminatedUnion("status", [
z.object({
status: z.literal("added"),
notificationDetails: notificationDetailsSchema.nullable(),
}),
z.object({
status: z.literal("removed"),
}),
]),
});
export type GETNotificationsDetailResponseBody = z.infer<
typeof getResponseBodySchema
>;
/**
* Gets the namespace settings
*/
export async function GET(
req: NextRequest,
{ params }: { params: { namespaceId: string } }
): Promise<Response> {
const storage = await getStorage(req.nextUrl.href);
const namespace = await storage.getNamespace(params.namespaceId);
if (!namespace) {
return Response.json({ message: "Not found" }, { status: 404 });
}
return Response.json(getResponseBodySchema.parse(namespace), {
status: 200,
});
}